﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows.Threading;

namespace Utils.Wpf
{
	public class BoundObservableCollection<T, TSource> : ObservableCollection<T>
	{
		private readonly object _addLock;

		private readonly ObservableCollection<TSource> _source;
		private readonly Func<TSource, T> _converter;
		private readonly Func<T, TSource, bool> _comparer;
        private readonly Dispatcher _currentDispatcher;

		public BoundObservableCollection(ObservableCollection<TSource> source, Func<TSource, T> converter, Func<T, TSource, bool> comparer)
		{
			_addLock = new object();
            _currentDispatcher = Dispatcher.CurrentDispatcher;
			_source = source;
			_converter = converter;
			_comparer = comparer;

			// Copy items
			this.AddItems(_source);

			// Subscribe to the source's CollectionChanged event
			_source.CollectionChanged += Source_CollectionChanged;
		}

		private void AddItems(IEnumerable<TSource> items)
		{
			foreach (TSource sourceItem in items)
			{
				lock (_addLock)
				{
					base.Add(_converter(sourceItem));	
				}
			}
		}

		void Source_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
		{
            if (_currentDispatcher.CheckAccess())
            {
				switch (e.Action)
				{
					case NotifyCollectionChangedAction.Add:
						this.AddItems(e.NewItems.Cast<TSource>());
						break;

					case NotifyCollectionChangedAction.Move:
						this.Move(e.OldStartingIndex, e.NewStartingIndex);
						break;

					case NotifyCollectionChangedAction.Remove:
						foreach (var sourceItem in e.OldItems.Cast<TSource>())
						{
							T toRemove = this.First(item => _comparer(item, sourceItem));
							this.Remove(toRemove);
						}
						break;

					case NotifyCollectionChangedAction.Replace:
						for (int i = e.NewStartingIndex; i < e.NewItems.Count; i++)
						{
							this[i] = _converter((TSource) e.NewItems[i]);
						}
						break;

					case NotifyCollectionChangedAction.Reset:
						this.Clear();
						this.AddItems(_source);
						break;

					default:
						break;
				}
            }
            else
            {
				if (e.Action == NotifyCollectionChangedAction.Reset) // This must be a synch call
					_currentDispatcher.Invoke(new Action<object, NotifyCollectionChangedEventArgs>(Source_CollectionChanged), new object[] { sender, e });
				else
					_currentDispatcher.BeginInvoke(new Action<object, NotifyCollectionChangedEventArgs>(Source_CollectionChanged), new object[] { sender, e });
            }
		}
	}

}
